/*
 * Copyright (c) 2011-2021, baomidou (jobob@qq.com).
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.baomidou.mybatisplus.samples.association.plugin;

import com.baomidou.mybatisplus.core.conditions.AbstractWrapper;
import com.baomidou.mybatisplus.core.toolkit.Constants;
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import com.baomidou.mybatisplus.samples.association.plugin.enums.ConditionEnum;
import com.baomidou.mybatisplus.samples.association.plugin.mapping.ParameterColumnMapping;
import com.baomidou.mybatisplus.samples.association.plugin.mapping.ParameterValueMapping;
import net.sf.jsqlparser.JSQLParserException;
import net.sf.jsqlparser.expression.Expression;
import net.sf.jsqlparser.expression.ExpressionVisitorAdapter;
import net.sf.jsqlparser.expression.operators.relational.Between;
import net.sf.jsqlparser.expression.operators.relational.EqualsTo;
import net.sf.jsqlparser.expression.operators.relational.ExpressionList;
import net.sf.jsqlparser.expression.operators.relational.GreaterThan;
import net.sf.jsqlparser.expression.operators.relational.GreaterThanEquals;
import net.sf.jsqlparser.expression.operators.relational.InExpression;
import net.sf.jsqlparser.expression.operators.relational.ItemsList;
import net.sf.jsqlparser.expression.operators.relational.LikeExpression;
import net.sf.jsqlparser.expression.operators.relational.MinorThan;
import net.sf.jsqlparser.expression.operators.relational.MinorThanEquals;
import net.sf.jsqlparser.expression.operators.relational.NotEqualsTo;
import net.sf.jsqlparser.parser.CCJSqlParserUtil;
import net.sf.jsqlparser.statement.Statement;
import net.sf.jsqlparser.statement.delete.Delete;
import net.sf.jsqlparser.statement.select.PlainSelect;
import net.sf.jsqlparser.statement.select.Select;
import net.sf.jsqlparser.statement.update.Update;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.executor.parameter.ParameterHandler;
import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.ParameterMapping;
import org.apache.ibatis.parsing.GenericTokenParser;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.reflection.SystemMetaObject;
import org.apache.ibatis.session.Configuration;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.lang.reflect.Proxy;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

/**
 * 插件工具类
 *
 * @author TaoYu , hubin
 * @since 2017-06-20
 */
public abstract class PluginUtils {
    private static final Logger LOGGER = LoggerFactory.getLogger(PluginUtils.class);
    public static final String DELEGATE_BOUNDSQL_SQL = "delegate.boundSql.sql";

    /**
     * 获得真正的处理对象,可能多层代理.
     */
    @SuppressWarnings("unchecked")
    public static <T> T realTarget(Object target) {
        if (Proxy.isProxyClass(target.getClass())) {
            MetaObject metaObject = SystemMetaObject.forObject(target);
            return realTarget(metaObject.getValue("h.target"));
        }
        return (T) target;
    }

    /**
     * 给 BoundSql 设置 additionalParameters
     *
     * @param boundSql             BoundSql
     * @param additionalParameters additionalParameters
     */
    public static void setAdditionalParameter(BoundSql boundSql, Map<String, Object> additionalParameters) {
        additionalParameters.forEach(boundSql::setAdditionalParameter);
    }

    public static MPBoundSql mpBoundSql(BoundSql boundSql) {
        return new MPBoundSql(boundSql);
    }

    public static MPStatementHandler mpStatementHandler(StatementHandler statementHandler) {
        statementHandler = realTarget(statementHandler);
        MetaObject object = SystemMetaObject.forObject(statementHandler);
        return new MPStatementHandler(SystemMetaObject.forObject(object.getValue("delegate")));
    }

    /**
     * {@link org.apache.ibatis.executor.statement.BaseStatementHandler}
     */
    public static class MPStatementHandler {
        private final MetaObject statementHandler;

        MPStatementHandler(MetaObject statementHandler) {
            this.statementHandler = statementHandler;
        }

        public ParameterHandler parameterHandler() {
            return get("parameterHandler");
        }

        public MappedStatement mappedStatement() {
            return get("mappedStatement");
        }

        public Executor executor() {
            return get("executor");
        }

        public MPBoundSql mPBoundSql() {
            return new MPBoundSql(boundSql());
        }

        public BoundSql boundSql() {
            return get("boundSql");
        }

        public Configuration configuration() {
            return get("configuration");
        }

        @SuppressWarnings("unchecked")
        private <T> T get(String property) {
            return (T) statementHandler.getValue(property);
        }
    }

    /**
     * {@link BoundSql}
     */
    public static class MPBoundSql {
        private final MetaObject boundSql;
        private final BoundSql delegate;

        MPBoundSql(BoundSql boundSql) {
            this.delegate = boundSql;
            this.boundSql = SystemMetaObject.forObject(boundSql);
        }

        public String sql() {
            return delegate.getSql();
        }

        public void sql(String sql) {
            boundSql.setValue("sql", sql);
        }

        public List<ParameterMapping> parameterMappings() {
            List<ParameterMapping> parameterMappings = delegate.getParameterMappings();
            return new ArrayList<>(parameterMappings);
        }

        public void parameterMappings(List<ParameterMapping> parameterMappings) {
            boundSql.setValue("parameterMappings", Collections.unmodifiableList(parameterMappings));
        }

        public Object parameterObject() {
            return get("parameterObject");
        }

        public Map<String, Object> additionalParameters() {
            return get("additionalParameters");
        }

        @SuppressWarnings("unchecked")
        private <T> T get(String property) {
            return (T) boundSql.getValue(property);
        }
    }

    /**
     * 获取要执行的SQL参数名、参数值、列名、表达式
     *
     * @param configuration Configuration
     * @param boundSql      BoundSql
     * @return sql完整语句
     */
    public static List<ParameterValueMapping> getParameterValueMapping(Configuration configuration, BoundSql boundSql) {
        List<ParameterValueMapping> parameterValueMappingList = new ArrayList<>();
        // 获取mapper里面方法上的参数
        Object sqlParameter = boundSql.getParameterObject();
        // sql语句里面需要的参数 -- 真实需要用到的参数 因为sqlParameter里面的每个参数不一定都会用到
        List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
        if (parameterMappings.size() > 0 && sqlParameter != null) {
            // mybatis plus语法
            boolean wrapperStatus = parameterMappings.stream().anyMatch(item -> item.getProperty().startsWith(Constants.WRAPPER));
            if (sqlParameter instanceof Map && wrapperStatus) {
                Map<String, Object> parameterObjectMap = (Map<String, Object>) sqlParameter;
                AbstractWrapper<?, ?, ?> abstractWrapper = (AbstractWrapper<?, ?, ?>) parameterObjectMap.getOrDefault(Constants.WRAPPER, null);
                return getParameterValueMappingList(abstractWrapper);
            } else {
                Map<String, Object> keyValueMap = getKeyValueMap(configuration, boundSql);
                List<ParameterColumnMapping> parameterColumnMappingList = getParameterColumnMappingList(boundSql);
                parameterValueMappingList = parameterColumnMappingList.stream().map(item -> {
                    ParameterValueMapping parameterValueMapping = new ParameterValueMapping();
                    parameterValueMapping.setParamName(item.getParamName());
                    parameterValueMapping.setColumnName(item.getColumnName());
                    parameterValueMapping.setConditionEnum(item.getConditionEnum());
                    parameterValueMapping.setParamValue(keyValueMap.get(item.getParamName()));
                    return parameterValueMapping;
                }).collect(Collectors.toList());
                return parameterValueMappingList;
            }
        }
        return parameterValueMappingList;
    }

    /**
     * 获取要执行的SQL参数名、参数值、列名、表达式
     * 一般获取boundSql.getParameterMappings即可，
     * 但是一些xml动态标签如：foreach {@link org.apache.ibatis.scripting.xmltags.ForEachSqlNode} itemizeItem 会动态生成标签 __frch_id_0,__frch_id_1
     * 则如果要匹配则需要进行转换
     * @param boundSql      BoundSql
     * @return sql完整语句
     */
    public static List<ParameterColumnMapping> getParameterColumnMappingList(BoundSql boundSql) {
        List<ParameterColumnMapping> parameterColumnMappingList = new ArrayList<>();
        // 获取mapper里面方法上的参数
        Object sqlParameter = boundSql.getParameterObject();
        // sql语句里面需要的参数
        List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
        if (parameterMappings.size() > 0 && sqlParameter != null) {
            try {
                String originalSql = boundSql.getSql();
                for (ParameterMapping parameterMapping : parameterMappings) {
                    originalSql = originalSql.replaceFirst("\\?", parameterMapping.getProperty());
                }
                // 执行节点条件逻辑处理
                Statement statement = CCJSqlParserUtil.parse(originalSql);
                if (statement instanceof Select) {
                    Select selectStatement = (Select) statement;
                    PlainSelect plainSelect = (PlainSelect) selectStatement.getSelectBody();
                    parameterColumnMappingList = getParameterColumnMappingList(plainSelect.getWhere());
                } else if (statement instanceof Update) {
                    Update update = (Update) statement;
                    parameterColumnMappingList = getParameterColumnMappingList(update.getWhere());
                } else if (statement instanceof Delete) {
                    Delete delete = (Delete) statement;
                    parameterColumnMappingList = getParameterColumnMappingList(delete.getWhere());
                }
                return parameterColumnMappingList;
            } catch (JSQLParserException e) {
                LOGGER.error("条件表达式解析出错： sql = {}", boundSql.getSql(), e);
                return parameterColumnMappingList;
            } catch (Exception e) {
                LOGGER.error("系统异常：", e);
                return parameterColumnMappingList;
            }
        }
        return parameterColumnMappingList;
    }

    public static Map<String, Object> getKeyValueMap(Configuration configuration, BoundSql boundSql) {
        Map<String, Object> keyValueMap = new HashMap<>();
        // 获取mapper里面方法上的参数
        Object sqlParameter = boundSql.getParameterObject();
        // sql语句里面需要的参数 -- 真实需要用到的参数 因为sqlParameter里面的每个参数不一定都会用到
        List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
        if (parameterMappings.size() > 0 && sqlParameter != null) {
            // 常用org.apache.ibatis.type.JdbcType数据类型处理
            if (configuration.getTypeHandlerRegistry().hasTypeHandler(sqlParameter.getClass())) {
                keyValueMap.put(parameterMappings.get(0).getProperty(), sqlParameter);
            } else {
                MetaObject metaObject = configuration.newMetaObject(sqlParameter);
                for (ParameterMapping parameterMapping : parameterMappings) {
                    String name = parameterMapping.getProperty();
                    if (metaObject.hasGetter(name)) {
                        Object valueObject = metaObject.getValue(name);
                        keyValueMap.put(name, valueObject);
                    } else if (boundSql.hasAdditionalParameter(name)) {
                        Object valueObject = boundSql.getAdditionalParameter(name);
                        keyValueMap.put(name, valueObject);
                    }
                }
            }
        }
        return keyValueMap;
    }

    /**
     * 获取查询条件封装真实的参数名，列名
     * abstractWrapper.getSqlSegment() 结果为(id = ew.paramNameValuePairs.MPGENVAL1 AND age IN (ew.paramNameValuePairs.MPGENVAL2,ew.paramNameValuePairs.MPGENVAL3))")
     *
     * @param abstractWrapper 查询条件封装对象
     * @return
     */
    public static List<ParameterValueMapping> getParameterValueMappingList(AbstractWrapper<?, ?, ?> abstractWrapper) {
        List<ParameterValueMapping> parameterValueMappingList = new ArrayList<>();
        if (abstractWrapper == null || StringUtils.isBlank(abstractWrapper.getSqlSegment())) {
            return parameterValueMappingList;
        }
        try {
            // 参数格式转换,对应SqlScriptUtils.safeParam 安全入参: #{入参}
            String sqlSegment = new GenericTokenParser(Constants.DOLLAR_LEFT_BRACE, Constants.RIGHT_BRACE, text -> text).parse(abstractWrapper.getSqlSegment());
            // 参数格式转换,对应SqlScriptUtils.unSafeParam 非安全入参:  ${入参}
            sqlSegment = new GenericTokenParser(Constants.HASH_LEFT_BRACE, Constants.RIGHT_BRACE, text -> text).parse(sqlSegment);
            Expression expression = CCJSqlParserUtil.parseCondExpression(sqlSegment);
            List<ParameterColumnMapping> parameterColumnMappingList = getParameterColumnMappingList(expression);
            parameterValueMappingList = parameterColumnMappingList.stream().map(item -> {
                ParameterValueMapping parameterValueMapping = new ParameterValueMapping();
                parameterValueMapping.setParamName(item.getParamName());
                parameterValueMapping.setColumnName(item.getColumnName());
                parameterValueMapping.setConditionEnum(item.getConditionEnum());
                // 需要去除前缀,格式参照AbstractWrapper.formatParam(String mapping, Object param)
                String prefix = abstractWrapper.getParamAlias() + Constants.WRAPPER_PARAM_MIDDLE;
                String paramKey = item.getParamName().replace(prefix, "");
                Object paramValue = abstractWrapper.getParamNameValuePairs().get(paramKey);
                parameterValueMapping.setParamValue(paramValue);
                return parameterValueMapping;
            }).collect(Collectors.toList());
            return parameterValueMappingList;
        } catch (JSQLParserException e) {
            LOGGER.error("条件表达式解析出错： sqlSegment = {}", abstractWrapper.getSqlSegment(), e);
            return parameterValueMappingList;
        } catch (Exception e) {
            LOGGER.error("系统出错 = {}", abstractWrapper.getSqlSegment(), e);
            return parameterValueMappingList;
        }
    }

    /**
     * 获取sql表达式参数名、列名映射列表
     *
     * @param expression sql表达式
     * @return
     */
    public static List<ParameterColumnMapping> getParameterColumnMappingList(Expression expression) {
        List<ParameterColumnMapping> parameterColumnMappingList = new ArrayList<>();
        if (expression == null) {
            return parameterColumnMappingList;
        }
        expression.accept(new ExpressionVisitorAdapter() {
            @Override
            public void visit(Between expr) {
                String columnName = expr.getLeftExpression().toString();
                String startParamName = expr.getBetweenExpressionStart().toString();
                String endParamName = expr.getBetweenExpressionEnd().toString();
                parameterColumnMappingList.add(new ParameterColumnMapping(startParamName, columnName, ConditionEnum.BETWEEN_START));
                parameterColumnMappingList.add(new ParameterColumnMapping(endParamName, columnName, ConditionEnum.BETWEEN_END));
            }

            @Override
            public void visit(EqualsTo expr) {
                parameterColumnMappingList.add(new ParameterColumnMapping(expr.getRightExpression().toString(), expr.getLeftExpression().toString(), ConditionEnum.EQUAL));
            }

            @Override
            public void visit(GreaterThan expr) {
                parameterColumnMappingList.add(new ParameterColumnMapping(expr.getRightExpression().toString(), expr.getLeftExpression().toString(), ConditionEnum.GREATER));
            }

            @Override
            public void visit(GreaterThanEquals expr) {
                parameterColumnMappingList.add(new ParameterColumnMapping(expr.getRightExpression().toString(), expr.getLeftExpression().toString(), ConditionEnum.GREATER_EQUAL));
            }

            @Override
            public void visit(InExpression expr) {
                ItemsList itemsList = expr.getRightItemsList();
                if (itemsList instanceof ExpressionList) {
                    ((ExpressionList) itemsList).getExpressions().forEach(item -> {
                        parameterColumnMappingList.add(new ParameterColumnMapping(item.toString(), expr.getLeftExpression().toString(), ConditionEnum.IN));
                    });
                }
            }

            @Override
            public void visit(LikeExpression expr) {
                parameterColumnMappingList.add(new ParameterColumnMapping(expr.getRightExpression().toString(), expr.getLeftExpression().toString(), ConditionEnum.LIKE));
            }

            @Override
            public void visit(MinorThan expr) {
                parameterColumnMappingList.add(new ParameterColumnMapping(expr.getRightExpression().toString(), expr.getLeftExpression().toString(), ConditionEnum.LESS));
            }

            @Override
            public void visit(MinorThanEquals expr) {
                parameterColumnMappingList.add(new ParameterColumnMapping(expr.getRightExpression().toString(), expr.getLeftExpression().toString(), ConditionEnum.LESS_EQUAL));
            }

            @Override
            public void visit(NotEqualsTo expr) {
                parameterColumnMappingList.add(new ParameterColumnMapping(expr.getRightExpression().toString(), expr.getLeftExpression().toString(), ConditionEnum.NOT_EQUAL));
            }
        });
        return parameterColumnMappingList;
    }
}
